# Copyright (C) 2007 Adriano Monteiro Marques <py.adriano@gmail.com>
#
# Author: Maxim Gavrilov <lovelymax@gmail.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#import pygtk
#pygtk.require('2.0')

import gettext
gettext.install('pysourceview')

import os, os.path

import gtk
from gtk import TextView, TextBuffer, TextIter
from gtk import gdk
import pango
import gobject
import pango

from xml.dom import minidom

# defines
GUTTER_PIXMAP = 16

TARGET_COLOR = 200 # DnD color 

MAX_CHARS_BEFORE_FINDING_A_MATCH = 2000
MIN_NUMBER_WINDOW_WIDTH = 20

DEFAULT_TAB_WIDTH = 8
MAX_TAB_WIDTH = 32

DEFAULT_MARGIN = 80
MAX_MARGIN = 200


SOURCEVIEW_DIR = "pysourceview-2.0"
RGN_SCHEMA_FILE = "language2.rgn"
LANGUAGE_DIR = "language-specs"
LANG_FILE_SUFFIX = ".lang"
DEFAULT_SECTION = _("Others")
GTK_SOURCE_LANGUAGE_VERSION_1_0 = 100
GTK_SOURCE_LANGUAGE_VERSION_2_0 = 200

# aux

def get_default_dirs(basename, compat):
    # TODO: dirs parsing
    return [os.path.join("pysourceview", basename)]

def get_file_list(dirs, suffix):
    result = []
    try:
        for path in dirs:
            for filename in os.listdir(path):
                fullpath = os.path.join(path, filename)
                if fullpath.endswith(suffix) and os.path.isfile(fullpath):
                    result.append(fullpath)
    except OSError:
        pass
    return result

def source_style_apply(style, tag):
    # TODO
    pass

# SourceContextData
class SourceContextData(gobject.GObject):
    def __init__(self, lang):
        gobject.GObject.__init__(self)

# SourceContextEngine
class SourceContextEngine(gobject.GObject):
    def __init__(self, data):
        gobject.GObject(self)

# SourceLanguage
class SourceLanguage(gobject.GObject):
    def new_from_file(self, filename, lm):
        def string_to_bool(string):
            return string in ("yes", "true", "1")
    
        def get_attr(node, name, default = None):
            if node.hasAttribute(name):
                return node.getAttribute(name)
            return None

        try:
            xml = minidom.parse(filename)
        except Exception, e:
            print filename, ':', e
            return None
        
        reader = xml.documentElement
        # process_language_node
        #lang = SourceLanguage()
        self.lang_file_name = filename
        self.translation_domain = get_attr(reader, "translation-domain")
        self.hidden = string_to_bool(get_attr(reader, "hidden"))
        tmp = get_attr(reader, "mimetypes")
        if tmp:
            self.properties["mimetypes"] = tmp
        tmp = get_attr(reader, "globs")
        if tmp:
            self.properties["globs"] = tmp
        tmp = get_attr(reader, "_name")
        if not tmp:
            tmp = get_attr(reader, "name")
            if not tmp:
                print "Impossible to get language name from file '%s'" % filename
                return None
            self.name = tmp
            untranslated_name = tmp
        else:
            self.name = tmp #TODO: translation
            untranslated_name = tmp
        self.id = get_attr(reader, "id", untranslated_name)
        tmp = get_attr(reader, "_section")
        if not tmp:
            self.section = get_attr(reader, "section", DEFAULT_SECTION)
        else:
            self.section = tmp # TODO: translation
        version = get_attr(reader, "version")
        if not version:
            print "Impossible to get version number from file '%s'" % filename
            return None
        if version == "1.0":
            self.version = GTK_SOURCE_LANGUAGE_VERSION_1_0
        elif version == "2.0":
            self.version = GTK_SOURCE_LANGUAGE_VERSION_2_0
        else:
            print "Unsupported lang spec version '%s' in filename '%s'" % (version, filename)
            return None

        if self.version == GTK_SOURCE_LANGUAGE_VERSION_2_0:
            # process_properties
            meta_node = None
            for node in reader.childNodes:
                if node.nodeType == node.ELEMENT_NODE and node.tagName == "metadata":
                    meta_node = node
                    break
            if meta_node:
                for child in meta_node.childNodes:
                    if child.nodeType == child.ELEMENT_NODE and child.tagName == "property":
                        name = get_attr(child, "name")
                        content = "".join([node.data for node in child.childNodes
                                           if node.nodeType == node.TEXT_NODE])
                        if name and content:
                            self.properties[name] = content
        self.language_manager = lm #weakref.ref(lm)
        return True
    
    def __init__(self):
        gobject.GObject.__init__(self)
        self.styles = dict()
        self.properties = dict()
        self.ctx_data = None

    def get_id(self):
        return self.id

    def get_name(self):
        return self.name

    def get_section(self):
        return self.section

    def get_metadata(self, name):
        return self.properties.get(name, None)

    def get_mime_types(self):
        mimetypes = self.get_metadata("mimetypes")
        if mimetypes is None:
            return None
        return mimetypes.split(";")

    def get_globs(self):
        globs = self.get_metadata("globs")
        if globs is None:
            return None
        return globs.split(";")

    def get_language_manager(self):
        return self.language_manager

    def _define_language_styles(self):
        self.styles["Base-N Integer"] = "def:base-n-integer"
        self.styles["Character"] = "def:character"
        self.styles["Comment"] = "def:comment"
        self.styles["Function"] = "def:function"
        self.styles["Decimal"] = "def:decimal"
        self.styles["Floating Point"] = "def:floating-point"
        self.styles["Keyword"] = "def:keyword"
        self.styles["Preprocessor"] = "def:preprocessor"
        self.styles["String"] = "def:string"
        self.styles["Specials"] = "def:specials"
        self.styles["Data Type"] = "def:data-type"

    def _create_engine(self):
        if not self.ctx_data:
            success = False
            if self.language_manager:
                ctx_data = SourceContextData(self)
                if self.version == GTK_SOURCE_LANGUAGE_VERSION_1_0:
                    success = self._file_parse_version1(ctx_data)
                elif self.version == GTK_SOURCE_LANGUAGE_VERSION_2_0:
                    success = self._file_parse_version2(ctx_data)
                if success:
                    self.ctx_data = ctx_data
        ce = None
        if self.ctx_data:
            ce = SourceContextEngine(self.ctx_data)
        return ce

    def _file_parse_version1(self, ctx_data):
        try:
            doc = minidom.parse(self.lang_file_name)
        except Exception, e:
            print "Impossible to parse file '%s'" % self.lang_file_name
            return False

        cur = doc.documentElement
        if cur.tagName != "language":
            print "File '%s' is of the wrong type" % self.lang_file_name
            return False
        if not cur.hasAttribute("version"):
            print "Language version missing in file '%s'" % self.lang_file_name
            return False
        else:
            lang_version = cur.getAttribute("version")
            if lang_version != "1.0":
                print "Wrong language version '%s' in file '%s', expected '%s'" % (lang_version, self.lang_version, "1.0")
                return False
        # define_root_context
        # XXXXXXXXXXX
            
            
            
        
    
# SourceLanguagesManager
class SourceLanguagesManager(gobject.GObject):
    __gproperties__ = {
        "search-path" : (gobject.TYPE_OBJECT,
                         _("Language specification directories"),
                         _("List of directories where the "
                           "language specification files (.lang) "
                           "are located"),
                         gobject.PARAM_READWRITE)
        }

    def __init__(self):
        gobject.GObject.__init__(self)
        self.lang_dirs = None
        self.language_ids = {}
        self.available_languages = []

    def get_property(self, property):
        if property.name == "search-path":
            return self.get_search_path()
        raise AttributeError, 'unknown property %s' % property.name
        
    def set_property(self, property, value):
        if property.name == "search-path":
            self.set_search_path(value)
        raise AttributeError, 'unknown property %s' % property.name

    def get_search_path(self):
        if not self.lang_dirs:
            return get_default_dirs(LANGUAGE_DIR, True)
        return self.lang_dirs

    def set_search_path(self, dirs):
        self.lang_dirs = dirs
        self.notify("search-path")

    def get_avaliable_languages(self):
        self._ensure_languages()
        return self.available_languages

    def get_language_by_id(self, id):
        self._ensure_languages()
        return self.language_ids[id]

    def get_rng_file(self):
        # TODO
        pass

    def _ensure_languages(self):
        if self.language_ids:
            return
        filename = get_file_list(self.get_search_path(), LANG_FILE_SUFFIX)
        for l in filename:
            lang = SourceLanguage()
            if lang.new_from_file(l, self) and not self.language_ids.has_key(lang.id):
                self.language_ids[lang.id] = lang
        self.available_languages = self.language_ids.values()
        
                         

# SourceUndoManager
class SourceUndoManager(gobject.GObject):
    pass

# SourceBuffer
class SourceBuffer(TextBuffer):
    __gproperties__ = {
        "check_brackets" : (gobject.TYPE_BOOLEAN,
                            _("Check Brackets"),
                            _("Whether to check and highlight matching brackets"),
                            True,
                            gobject.PARAM_READWRITE),
        
        "highlight" : (gobject.TYPE_BOOLEAN,
                       _("Highlight"),
                       _("Whether to highlight syntax in the buffer"),
                       True,
                       gobject.PARAM_READWRITE),

        "max_undo_levels" : (gobject.TYPE_UINT,
                             _("Maximum Undo LEvels"),
                             _("Number of undo levels for the buffer"),
                             0,
                             200,
                             25,
                             gobject.PARAM_READWRITE),

        "language" : (gobject.TYPE_OBJECT,
                      _("Language"),
                      _("Language obejct to get highlighting patterns from"),
                      gobject.PARAM_READWRITE),

        "can-undo" : (gobject.TYPE_BOOLEAN,
                      _("Can undo"),
                      _("Whether Undo operation is possible"),
                      False,
                      gobject.PARAM_READWRITE),

        "can-redo" : (gobject.TYPE_BOOLEAN,
                      _("Can redo"),
                      _("Whether Redo operation is possible"),
                      False,
                      gobject.PARAM_READWRITE)
    }

    __gsignals__ = {
        'highlight_updated' : (gobject.SIGNAL_RUN_LAST,
                               gobject.TYPE_NONE,
                               (TextIter, TextIter)),
        'marker_updated' : (gobject.SIGNAL_RUN_LAST,
                            gobject.TYPE_NONE,
                            (TextIter,))
    }

    def __init__(self, table=None):
        TextBuffer.__init__(self)

        self.undo_manager = SourceUndoManager()
        self.check_brackets = True
        self.bracket_mark = None
        self.bracket_found = False
        self.bracket_match_tag = None
        self.markers = []
        self.highlight = True
        self.highlight_engine = None
        # TODO
        #self.style_scheme =
        self.connect("mark_set", self.move_cursor)
        #self.undo_manager.connect("can_undo", self.can_undo_handler)
        #self.undo_manager.connect("can_redo", self.can_redo_hadler)
        

    def set_check_brackets(self, check_brackets):
        if check_brackets != self.check_brackets:
            self.check_brackets = check_brackets
            self.notify("check_brackets")

    def get_check_brackets(self):
        return self.check_brackets

    def set_highlight(self, highlight):
        if highlight != self.highlight:
            self.highlight = highlight
            self.notify("highlight")

    def get_highlight(self):
        return self.highlight

    def set_max_undo_levels(self, max_undo_levels):
        if self.undo_manager.get_max_undo_levels() != max_undo_levels:
            self.undo_manager.set_max_undo_levels(max_undo_levels)
            self.notify("max_undo_levels")

    def get_max_undo_levels(self):
        return self.undo_manager.get_max_undo_levels()

    def set_language(self, language):
        if self.language == language:
            return
        if self.highlight_engine:
            self.highlight_engine.attach_buffer(None)
            self.highlight_engine = None
        self.language = language
        if language:
            self.highlight_engine = language.create_engine()
            if self.highlight_engine:
                self.highlight_engine.attach_buffer(self)
                #if self.style_scheme:
                #    self.highlight_engine.set_style_scheme(self.style_scheme)

    def get_language(self):
        return self.language

    def can_undo(self):
        return self.undo_manager.can_undo()

    def can_redo(self):
        return self.undo_manager.can_redo()
        
    _property_map = {
        'check-brackets': (set_check_brackets, get_check_brackets),
        'highlight' : (set_highlight, get_highlight),
        'max-undo-levels' : (set_max_undo_levels, get_max_undo_levels),
        'language' : (set_language, get_language),
        'can-undo' : (None, can_undo),
        'can-redo' : (None, can_redo)
        # TODO
        #'style-scheme' : (set_style_scheme, get_style_scheme)
    }
    
    def get_property(self, property):
        if property.name in self._property_map:
            f = self._property_map[property.name]
            if f:
                return f()
        raise AttributeError, 'unknown property %s' % property.name
        
    def set_property(self, property, value):
        if property.name in self._property_map:
            f = self._property_map[property.name]
            if f:
                return f(value)
        raise AttributeError, 'unknown property %s' % property.name

    # methods
    def update_highlight(self, start, end, synchronous):
        if self.highlight_engine:
            self.highlight_engine.update_highlight(start, end, synchronous)

    # handlers
    #def do_mark_set(self, it, mark):
    #    self.move_cursor(self, it,mark)
    #    TextBuffer.do_mark_set(self, it, mark)
        
    def move_cursor(self, buffer, it, mark):
        # TODO: some strange with marks :-(
        #print mark.get_name(), self.get_insert().get_name()
        if mark != self.get_insert():
            return
        #if not it.equal(self.get_iter_at_mark(self.get_insert())):
        #    return
        #print 'Yep'
        if self.bracket_found:
            it1 = self.get_iter_at_mark(self.bracket_mark)
            it2 = it1.copy()
            it2.forward_char()
            self.remove_tag(self.bracket_match_tag, it1, it2)

        if not self.check_brackets:
            return

        it1 = it.copy()
        if self._find_bracket_match_with_limit(it1, MAX_CHARS_BEFORE_FINDING_A_MATCH):
            if not self.bracket_mark:
                self.bracket_mark = self.create_mark(None, it1, False)
            else:
                self.move_mark(self.bracket_mark, it1)
            it2 = it1.copy()
            it2.forward_char()
            self.apply_tag(self._get_bracket_match_tag(), it1, it2)
            self.bracket_found = True
        else:
            self.bracket_found = False

    def _find_bracket_match_real(self, orig, max_chars):
        it = orig.copy()
        cur_char = it.get_char()
        base_char = cur_char
        left = '{([<'
        right= '})]>'
        if base_char in left:
            addition = 1
            search_char = right[left.find(base_char)]
        elif base_char in right:
            addition = -1
            search_char = left[right.find(base_char)]
        else:
            return
        counter = 0
        found = False
        char_cont = 0
        while True:
            it.forward_chars(addition)
            cur_char = it.get_char()
            char_cont += 1
            if cur_char == search_char or cur_char == base_char:
                if cur_char == search_char and counter == 0:
                    found = True
                    break
                if cur_char == base_char:
                    counter += 1
                else:
                    counter -= 1
            #print char_cont, cur_char, search_char, counter
            if it.is_end() or it.is_start() or (char_cont >= max_chars and max_chars >= 0):
                break
        if found:
            orig = it.copy()
        return found

    def _find_bracket_match_with_limit(self, orig, max_chars):
        if self._find_bracket_match_real(orig, max_chars):
            return True

        it = orig.copy()
        if not it.starts_line() and it.backward_char():
            if self._find_bracket_match_real(it, max_chars):
                orig = it
                return True
        return False


    def _get_bracket_match_tag(self):
        if not self.bracket_match_tag:
            self.bracket_match_tag = self.create_tag(None)
            self._update_bracket_match_style()
        return self.bracket_match_tag

    def _update_bracket_match_style(self):
        if self.bracket_match_tag:
            style = None
            #if self.style_scheme:
            #    style = self.style_scheme.get_matching_brackets_style()
            source_style_apply(style, self.bracket_match_tag)

    def can_undo_handler(self, um, can_undo):
        self.notify("can-undo")

    def can_redo_hadler(self, um, can_redo):
        self.notify("can-redo")
        
    
# SourceView
class SourceView(TextView):
    __gproperties__ = {
        "show_line_numbers" : (gobject.TYPE_BOOLEAN,
                               _("Show Line Numbers"),
                               _("Whether to display line numbers"),
                               False,
                               gobject.PARAM_READWRITE),
        
        "show_line_markers" : (gobject.TYPE_BOOLEAN,
                               _("Show Line Markers"),
                               _("Whether to display line marker pixbufs"),
                               False,
                               gobject.PARAM_READWRITE),

        "tabs_width" : (gobject.TYPE_UINT,
                        _("Tabs Width"),
                        _("Tabs Width"),
                        1,
                        MAX_TAB_WIDTH,
                        DEFAULT_TAB_WIDTH,
                        gobject.PARAM_READWRITE),
        
        "auto_indent" : (gobject.TYPE_BOOLEAN,
                         _("Auto Indentation"),
                         _("Whether to enable auto indentation"),
                         False,
                         gobject.PARAM_READWRITE),

        "insert_spaces_instead_of_tabs": (gobject.TYPE_BOOLEAN,
                                          _("Insert Spaces Instead of Tabs"),
                                          _("Whether to insert spaces instead of tabs"),
                                          False,
                                          gobject.PARAM_READWRITE),

        "show_margin" : (gobject.TYPE_BOOLEAN,
                         _("Show Right Margin"),
                         _("Whether to display the right margin"),
                         False,
                         gobject.PARAM_READWRITE),

        "margin" : (gobject.TYPE_UINT,
                    _("Margin position"),
                    _("Position of the right margin"),
                    1,
                    MAX_MARGIN,
                    DEFAULT_MARGIN,
                    gobject.PARAM_READWRITE),

        "smart_home_end" : (gobject.TYPE_BOOLEAN,
                            _("Smart Home/End"),
                            _("HOME and END keys move to first/last "
                              "non whitespace chapters on line before going "
                              "to the start/end of the line"),
                            False,
                            gobject.PARAM_READWRITE),

        "highlight_current_line" : (gobject.TYPE_BOOLEAN,
                                    _("Highlight current line"),
                                    _("Whether to highlight the current line"),
                                    False,
                                    gobject.PARAM_READWRITE),

        "indent_on_tab" : (gobject.TYPE_BOOLEAN,
                           _("Indent on tab"),
                           _("Whether to indent the selected text when the tab key is pressed"),
                           True,
                           gobject.PARAM_READWRITE)
    }

    
    def __init__(self, buffer=None):
        TextView.__init__(self, buffer)
        
        self.tabs_width = DEFAULT_TAB_WIDTH
        self.margin = DEFAULT_MARGIN;
        self.cached_margin_width = -1
        self.indent_on_tab = True
        self.smart_home_end = False
        self.set_left_margin(2)
        self.set_right_margin(2)

        # create members
        self.show_line_numbers = False
        self.show_line_markers = False
        self.auto_indent = False
        self.insert_spaces = False
        self.show_margin = False
        self.highlight_current_line = False
        self.source_buffer = None
        self.source_buffer_handler_id = []
        self.old_lines = 0 # number of lines
        self.current_line_gc = None

        self._set_source_buffer(self.get_buffer())

        # TODO: not yet implemented
        #self.pixmap_cache = dict()
        #self.style_scheme_applied = False
        #self.style_scheme = None

        # DnD init
        #tl = self.drag_dest_get_target_list()
        #if tl:
        #    tl.append(("application/x-color", 0, TARGET_COLOR))
        #    self.connect("drag_data_received", self.view_dnd_drop)
        #    self.connect("notify::buffer", self.notify_buffer0
        
    def set_show_line_numbers(self, show):
        if show and not self.show_line_numbers:
            if not self.show_line_markers:
                self.set_border_window_size(gtk.TEXT_WINDOW_LEFT, MIN_NUMBER_WINDOW_WIDTH)
            else:
                self.queue_draw()
            self.show_line_numbers = show
            self.notify("show_line_numbers")
        if not show and self.show_line_numbers:
            self.queue_draw()
            self.show_line_numbers = show
            self.notify("show_line_numbers")

    def get_show_line_numbers(self):
        return self.show_line_numbers

    def set_show_line_markers(self, show):
        if show and not self.show_line_markers:
            if not self.show_line_numbers:
                self.set_border_window_size(gtk.TEXT_WINDOW_LEFT, MIN_NUMBER_WINDOW_WIDTH)
            else:
                self.queue_draw()
            self.show_line_markers = show
            self.notify("show_line_markers")
        if not show and self.show_line_markers:
            self.queue_draw()
            self.show_line_markers = show
            self.notify("show_line_markers")

    def get_show_line_markers(self):
        return self.show_line_markers

    def set_tabs_width(self, width):
        if width <= 0 or width > MAX_TAB_WIDTH or self.tabs_width == width:
            return
        save_width = self.tabs_width
        self.tabs_width = width
        if self._set_tab_stops_internal():
            self.notify("tabs_width")
        else:
            self.tabs_width = save_width

    def get_tabs_width(self):
        return self.tabs_width

    def set_auto_indent(self, enable):
        if self.auto_indent != enable:
            self.auto_indent = enable
            self.notify("auto_indent")
            
    def get_auto_indent(self):
        return self.auto_indent

    def set_insert_spaces_instead_of_tabs(self, enable):
        if self.insert_spaces != enable:
            self.insert_spaces = enable
            self.notify("insert_spaces_instead_of_tabs")

    def get_insert_spaces_instead_of_tabs(self):
        return self.insert_spaces

    def set_show_margin(self, show):
        if self.show_margin != show:
            self.show_margin = show
            self.queue_draw()
            self.notify("show_margin")

    def get_show_margin(self):
        return self.show_margin

    def set_margin(self, margin):
        if margin < 1 or margin > MAX_MARGIN or self.margin == margin:
            return
        self.margin = margin
        self.cached_margin_width = -1
        self.queue_draw()
        self.notify("margin")

    def get_margin(self):
        return self.margin

    def set_smart_home_end(self, enable):
        if not self.smart_home_end == enable:
            self.smart_home_end = enable
            self.notify("smart_home_end")

    def get_smart_home_end(self):
        return self.smart_home_end

    def set_highlight_current_line(self, enable):
        if not self.highlight_current_line == enable:
            self.highlight_current_line = enable
            self.queue_draw()
            self.notify("highlight_current_line")
    
    def get_highlight_current_line(self):
        return self.highlight_current_line

    _property_map = {
        'show-line-numbers': (set_show_line_numbers, get_show_line_numbers),
        'show-line-marker': (set_show_line_markers, get_show_line_markers),
        'tabs-width': (set_tabs_width, get_tabs_width),
        'auto-indent': (set_auto_indent, get_auto_indent),
        'insert-spaces-instead-of-tabs': (set_insert_spaces_instead_of_tabs, get_insert_spaces_instead_of_tabs),
        'show-margin': (set_show_margin, get_show_margin),
        'margin': (set_margin, get_margin),
        'smart-home-end': (set_smart_home_end, get_smart_home_end),
        'highlight-current-line': (set_highlight_current_line, get_highlight_current_line)
        }
    
    def get_property(self, property):
        if property.name in self._property_map:
            f = self._property_map[property.name]
            if f:
                return f()
        raise AttributeError, 'unknown property %s' % property.name
        
    def set_property(self, property, value):
        if property.name in self._property_map:
            f = self._property_map[property.name]
            if f:
                return f(value)
        raise AttributeError, 'unknown property %s' % property.name
        
    # aux
    def _set_tab_stops_internal(self):
        real_tab_width = self._calculate_real_tab_width(self.tabs_width, ' ')
        if real_tab_width < 0:
            return False
        tab_array = pango.TabArray(1, True)
        tab_array.set_tab(0, pango.TAB_LEFT, real_tab_width)
        self.set_tabs(tab_array)
        return True

    def _calculate_real_tab_width(self, tab_size, c):
        if tab_size == 0:
            return -1
        tab_string = c * tab_size
        layout = self.create_pango_layout(tab_string)
        if layout:
            return layout.get_pixel_size()[0]
        return -1

    def _set_source_buffer(self, buffer):
        if self.source_buffer == buffer:
            return
        if self.source_buffer:
            for handler_id in self.source_buffer_handler_id:
                self.source_buffer.disconnect(handler_id)
        if buffer and type(buffer) == SourceBuffer:
            self.source_buffer = buffer
            self.source_buffer_handler_id = [
                buffer.connect("highlight_updated", self._highlight_updated_cb),
                buffer.connect("marker-updated", self._marker_updated_cb),
                buffer.connect("notify::style_scheme", self._buffer_style_scheme_changed_cb),
            ]
        else:
            self.source_buffer = None
            self.source_buffer_handler_id = []

        if buffer:
            self._update_style_scheme()

    def _highlight_updated_cb(self, buffer, start, end):
        visible_rect = self.get_visible_rect()
        (y, height) = self.get_line_yrange(start)
        updated_rect.y = y
        (y, height) = self.get_line_yrange(end)
        updated_rect.height = y + height - updated_rect.y
        updated_rect.x = visible_rect.x
        updated_rect.width = visible_rect.width

        redraw_rect = updated_rect.intersect(visible_rect)
        if not tuple(redraw_rect) == (0, 0, 0, 0):
            (x, y) = self.buffer_to_window_coord(gtk.TEXT_WINDOW_WIDGET, redraw_rect.x, redraw_rect.y)
            widget_rect = gdk.Rectangle(0, 0, redraw_rect.width, redraw_rect.height)
            self.query_draw_area(widget_rect.x, widget_rect.y, widget_rect.width, widget_rect.height)

    def _marker_updated_cb(self, buffer, where):
        if not self.show_line_markers:
            return
        visible_rect = self.get_visible_rect()
        (y, height) = self.get_line_yrange(where)
        updated_rect = gdk.Rectangle(visible_rect.x, y, visible_rect.width, height)
        redraw_rect = updated_rect.intersect(visible_rect)
        if not tuple(redraw_rect) == (0, 0, 0, 0):
            y_win = self.buffer_to_window_coord(gtk.TEXT_WINDOW_WIDGET, 0, redraw_rect.y)[1]
            width = self.get_border_window_size(gtk.TEXT_WINDOW_LEFT)
            self.query_draw_area(0, y_win, width, height)
        
    def _buffer_style_scheme_changed_cb(self, buffer, pspec):
        self._update_style_scheme()

    def _update_style_scheme(self):
        # TODO: not yet implemented
        pass
        #buffer = self.get_buffer()
        #if type(buffer) == SourceBuffer:
        #    new_scheme = buffer.get_style_scheme()
        #else:
        #    new_scheme = None

    def do_key_press_event(self, event):
        buf = self.get_buffer()
        modifiers = gtk.accelerator_get_default_mod_mask()
        key = event.keyval
        mark = buf.get_insert()
        cur = buf.get_iter_at_mark(mark)
        if key in [gdk.keyval_from_name(name) for name in ['Enter', 'Return']] and \
           not event.state & gdk.SHIFT_MASK and \
           self.auto_indent:
            indent = self._compute_indentation(cur)
            if indent:
                # TODO: find im_context
                #if self.im_context.filter_keypress(event):
                #    return True
                cur = buf.get_iter_at_mark(mark)
                buf.begin_user_action()
                buf.insert(cur, "\n")
                buf.insert(cur, indent)
                buf.end_user_action()
                self.scroll_mark_onscreen(mark)
                return True

        if key in [gdk.keyval_from_name(name) for name in ['Tab', 'KP_Tab', 'ISO_Left_Tab']] and \
           (event.state & modifiers) in (0, gdk.SHIFT_MASK):
            selection = buf.get_selection_bounds()
            has_selection = bool(selection)
            if not has_selection:
                it = buf.get_iter_at_mark(buf.get_insert())
                selection = (it, it.copy())
            
            if self.indent_on_tab:
                if event.state & gdk.SHIFT_MASK:
                    self._unindent_lines(selection)
                    return True
                if has_selection and \
                   ((selection[0].starts_line() and selection[1].ends_line()) or \
                    (selection[0].get_line() != selection[1].get_line())):
                    self._indent_lines(selection)
                    return True
            self._insert_tab_or_spaces(selection)
            return True
        return TextView.do_key_press_event(self, event)

    def _unindent_lines(self, (start, end)):
        buf = self.get_buffer()
        start_line = start.get_line()
        end_line = end.get_line()

        if end.get_visible_line_offset() == 0 and end_line > start_line:
            end_line -= 1

        buf.begin_user_action()
        for i in range(start_line, end_line + 1):
            it = buf.get_iter_at_line(i)
            if it.get_char == '\t':
                it2 = it.copy()
                it2.forward_char()
                buf.delete(it, it2)
            elif it.get_char() == ' ':
                spaces = 0
                it2 = it.copy()
                while not it2.ends_line():
                    if it2.get_char() == ' ':
                        spaces += 1
                    else:
                        break
                    it2.forward_char()
                if spaces > 0:
                    spaces = spaces % self.tabs_width
                    if spaces == 0:
                        spaces = self.tabs_width
                    it2 = it.copy()
                    it2.forward_chars(spaces)
                    buf.delete(it, it2)
        buf.end_user_action()
        self.scroll_mark_onscreen(buf.get_insert())


    def _compute_indentation(self, cur):
        line = cur.get_line()
        buf = self.get_buffer()
        start = buf.get_iter_at_line(line)
        end = start.copy()
        ch = end.get_char()

        while ch.isspace() and not ch in ('\n', '\r') and end.compare(cur) < 0:
            if not end.forward_char():
                break
            ch = end.get_char()

        if start.equal(end):
            return None

        res = start.get_slice(end)
        return res

    def _indent_lines(self, (start, end)):
        buf = self.get_buffer()

        start_line = start.get_line()
        end_line = end.get_line()

        if end.get_visible_line_offset() == 0 and end_line > start_line:
            end_line -= 1

        if self.get_insert_spaces_instead_of_tabs():
            tabs_size = self.tabs_width
            tab_buffer = ' ' * tabs_size
        else:
            tab_buffer = "\t"

        buf.begin_user_action()
        for i in xrange(start_line, end_line + 1):
            cur = buf.get_iter_at_line(i)
            if cur.ends_line():
                continue
            buf.insert(cur, tab_buffer, -1)
        buf.end_user_action()

        self.scroll_mark_onscreen(buf.get_insert())
        
    def _insert_tab_or_spaces(self, (start, end)):
        if self.insert_spaces:
            tabs_size = self.tabs_width
            cur = start.copy()
            cur_pos = cur.get_line_offset()
            tab_pos = cur_pos
            while tab_pos > 0:
                cur.backward_char()
                c = cur.get_char()
                if c == '\t':
                    break
                tab_pos -= 1
            num_of_equivalent_spaces = tabs_size - (cur_pos - tab_pos) % tabs_size
            tab_buf = ' ' * num_of_equivalent_spaces
        else:
            tab_buf = '\t'

        buf = self.get_buffer()
        buf.begin_user_action()
        buf.delete(start, end)
        buf.insert(start, tab_buf, -1)
        buf.end_user_action()

    def do_move_cursor(self, step, count, extend_selection):
        buffer = self.get_buffer()
        mark = buffer.get_insert()
        cur = buffer.get_iter_at_mark(mark)
        it = cur.copy()

        if self.smart_home_end and step == gtk.MOVEMENT_DISPLAY_LINE_ENDS and count == -1:
            #move_to_first_char
            it.set_line_offset(0)
            while not it.ends_line():
                if it.get_char().isspace():
                    it.forward_char()
                else:
                    break
            self._do_cursor_move(cur, it, extend_selection)
            return
        elif self.smart_home_end and step == gtk.MOVEMENT_DISPLAY_LINE_ENDS and count == 1:
            #move_to_last_char
            if not it.ends_line():
                it.forward_to_line_end()
            while not it.starts_line():
                it.backward_char()
                if not it.get_char().isspace():
                    it.forward_char()
                    break
            self._do_cursor_move(cur, it, extend_selection)
            return
        TextView.do_move_cursor(self, step, count, extend_selection)
                
    def _do_cursor_move(self, cur, it, extend_selection):
        buffer = self.get_buffer()
        if not cur.equal(it) or not extend_selection:
            # move_cursor
            if extend_selection:
                buffer.move_mark_by_name("insert", it)
            else:
                buffer.place_cursor(it)
            self.scroll_mark_onscreen(buffer.get_insert())

    def do_expose_event(self, event):
        event_handled = False
        # update highlight
        if event.window == self.get_window(gtk.TEXT_WINDOW_TEXT) and not self.source_buffer is None:
            visible_rect = self.get_visible_rect()
            iter1 = self.get_line_at_y(visible_rect.y)[0]
            iter1.backward_line()
            iter2 = self.get_line_at_y(visible_rect.y + visible_rect.height)[0]
            iter2.forward_line()
            self.source_buffer.update_highlight(iter1, iter2, False)

        if event.window == self.get_window(gtk.TEXT_WINDOW_LEFT):
            self._paint_margin(event)
            event_handled = True
        else:
            buffer =self.get_buffer()
            lines = buffer.get_line_count()
            if self.old_lines != lines:
                self.old_lines = lines
                w = self.get_window(gtk.TEXT_WINDOW_LEFT)
                if not w is None:
                    w.invalidate_rect(None, False)

            if self.highlight_current_line and event.window == self.get_window(gtk.TEXT_WINDOW_TEXT):
                cur = buffer.get_iter_at_mark(buffer.get_insert())
                (y, height) = self.get_line_yrange(cur)
                visible_rect = self.get_visible_rect()
                (redraw_x, redraw_y) = self.buffer_to_window_coords(gtk.TEXT_WINDOW_TEXT,
                                                                              visible_rect.x, visible_rect.y)
                (win_x, win_y) = self.buffer_to_window_coords(gtk.TEXT_WINDOW_TEXT, 0, y)
                redraw_rect = gdk.Rectangle(redraw_x, redraw_y, visible_rect.width, visible_rect.height)

                if self.current_line_gc:
                    gc = self.current_line_gc
                else:
                    gc = self.style.bg_gc[self.state]

                if self.get_focus_hadjustment():
                    margin = self.get_left_margin() - int(self.get_focus_hadjustment())
                else:
                    margin = self.get_left_margin()

                event.window.draw_rectangle(gc, True, redraw_rect.x + max(0, margin - 1),
                                   win_y, redraw_rect.width, height)

            event_handled = TextView.do_expose_event(self, event)

            if self.show_margin and event.window == self.get_window(gtk.TEXT_WINDOW_TEXT):
                if self.cached_margin_width < 0:
                    self.cached_margin_width = self._calculate_real_tab_width(self.margin, '_')
                visible_rect = self.get_visible_rect()
                redraw_x, redraw_y = self.buffer_to_window_coords(gtk.TEXT_WINDOW_LEFT,
                                                                  visible_rect.x, visible_rect.y)
                redraw_rect = gdk.Rectangle(redraw_x, redraw_y, visible_rect.width, visible_rect.height)
                cr = self.get_window(gtk.TEXT_WINDOW_TEXT).cairo_create()
                cr.rectangle(event.area.x, event.area.y, event.area.width, event.area.height)
                cr.clip()
                x = self.cached_margin_width - \
                    visible_rect.x + redraw_rect.x + 0.5 + \
                    self.get_left_margin()
                cr.set_line_width(1.0)
                cr.move_to(x, redraw_rect.y)
                cr.line_to(x, redraw_rect.y + redraw_rect.height)
                # TODO: get style properties
                alpha = 40
                line_color = 0
                toggle = False

                if not line_color:
                    line_color = self.style.text[gtk.STATE_NORMAL]
                cr.set_source_rgba(line_color.red / 65535.,
                                   line_color.green / 65535.,
                                   line_color.blue / 65535.,
                                   alpha / 255.)
                cr.stroke()
                if toggle:
                    # TODO: overlay draw
                    pass
        return event_handled

    def do_button_press_event(self, event):
        buf = self.get_buffer()
        if self.show_line_numbers and event.window == self.get_window(gtk.TEXT_WINDOW_LEFT):
            (x_buf, y_buf) = self.window_to_buffer_coord(gtk.TEXT_WINDOW_LEFT, event.x, event.y)
            line_start = self.get_line_at_y(y_buf)[0]
            if event.type == gdk.BUTTON_PRESS and event.button == 1:
                if event.state & gdk.CONTROL_MASK:
                    self._select_line(buf, line_start)
                elif event.state & gdk.SHIFT_MASK:
                    self._extend_selection_to_line(buf, line_start)
                else:
                    buf.place_cursor(line_start)
            elif event.type == gdk._2BUTTON_PRESS and event.button == 1:
                self._select_line(buf, line_start)
            return True
        return TextView.do_button_press_event(self, event)

    def _select_line(self, buf, line_start):
        # TODO: to implement
        pass

    def _extend_selection_to_line(self, buf, line_start):
        # TODO: to implement
        pass
    
    def _paint_margin(self, event):
        if not self.show_line_numbers and not self.show_line_markers:
            self.set_border_window_size(gtk.TEXT_WINDOW_LEFT, 0)
            return
        win = self.get_window(gtk.TEXT_WINDOW_LEFT)
        buf = self.get_buffer()
        y1 = event.area.y
        y2 = y1 + event.area.height
        (x1, y1) = self.window_to_buffer_coords(gtk.TEXT_WINDOW_LEFT, 0, y1)
        (x2, y2) = self.window_to_buffer_coords(gtk.TEXT_WINDOW_LEFT, 0, y2)

        (numbers, pixels) = self._get_lines(y1, y2)

        if len(numbers) == 0:
            numbers.append(0)
            pixels.append(0)

        tmp = str(max(99, buf.get_line_count()))
        layout = self.create_pango_layout(tmp)
        text_width = layout.get_pixel_size()[0]
        layout.set_width(text_width)
        layout.set_alignment(pango.ALIGN_RIGHT)

        if self.show_line_numbers:
            margin_width = text_width + 4
        else:
            margin_width = 0
        x_pixmap = margin_width
        if self.show_line_markers:
            margin_width += GUTTER_PIXMAP
        if margin_width == 0:
            return

        self.set_border_window_size(gtk.TEXT_WINDOW_LEFT, margin_width)
        markers = None
        if self.source_buffer and self.show_line_markers:
            # TODO: not yet implemented
            pass

        #current_marker = markers
        #if current_marker:
        #    marker_line = current_marker.data.get_line()

        cur = buf.get_iter_at_mark(buf.get_insert())
        cur_line = cur.get_line() + 1
        for number, pixel in zip(numbers, pixels):
            pos = self.buffer_to_window_coords(gtk.TEXT_WINDOW_LEFT, 0, pixel)[1]
            if self.show_line_numbers:
                line_to_paint = number + 1
                if line_to_paint == cur_line:
                    layout.set_markup("<b>%d</b>" % line_to_paint)
                else:
                    layout.set_markup("%d" % line_to_paint)
                self.style.paint_layout(win, self.state, False, None, self, None, text_width + 2, pos, layout)

            if self.show_line_markers and current_marker and marker_line == number:
                # TODO: _draw_line_markers
                pass
        

    def _get_lines(self, first_y, last_y):
        buffer_coords = []
        numbers = []
        last_line_num = -1
        it = self.get_line_at_y(first_y)[0]

        while not it.is_end():
            (y, height) = self.get_line_yrange(it)
            buffer_coords.append(y)
            last_line_num = it.get_line()
            numbers.append(last_line_num)
            if y + height >= last_y:
                break
            it.forward_line()

        if it.is_end():
            (y, height) = self.get_line_yrange(it)
            line_num = it.get_line()
            if line_num != last_line_num:
                buffer_coords.append(y)
                numbers.append(line_num)

        return (numbers, buffer_coords)
            

# TODO: is it really needed?
#gobject.type_register(SourceBuffer)
#gobject.type_register(SourceView)

if __name__ == "__main__":
    import gtk
    class TestApp:
        def destroy(self, widget, data=None):
            gtk.main_quit()

        def __init__(self):
            self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
            self.window.set_size_request(400, 400)
            self.window.connect("destroy", self.destroy)
            
            self.mng = SourceLanguagesManager()
            print [lang.name for lang in self.mng.get_avaliable_languages()]
            
            self.textbuf = SourceBuffer()
            self.textbuf.set_check_brackets(True)
            
            self.textview = SourceView(self.textbuf)
            self.textview.set_tabs_width(10)
            self.textview.set_auto_indent(True)
            self.textview.set_insert_spaces_instead_of_tabs(True)
            self.textview.set_smart_home_end(True)
            self.textview.set_highlight_current_line(True)
            self.textview.set_show_line_numbers(True)
            self.textview.set_margin(80)
            self.textview.set_show_margin(True)
            self.window.add(self.textview)
            self.window.show_all()

    app = TestApp()
    gtk.main()

